﻿
namespace GSBA.DataImporter
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Xml;

    public sealed class DataTable<TRow> : IEnumerable<TRow>
    {
        private readonly string name;
        private readonly List<TRow> rows;

        internal DataTable(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            this.name = name;
            this.rows = new List<TRow>();
        }

        public string Name
        {
            get { return this.name; }
        }

        public int Count
        {
            get { return this.rows.Count; }
        }

        public IEnumerator<TRow> GetEnumerator()
        {
            return this.rows.AsReadOnly().GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)this.rows.AsReadOnly()).GetEnumerator();
        }

        internal void Add(TRow row)
        {
            this.rows.Add(row);
        }
    }

    internal static class DataTable
    {
        private const string RowsetNamespace = "urn:schemas-microsoft-com:rowset";
        private const string RowsetSchemaNamespace = "#RowsetSchema";

        private static readonly Dictionary<Type, RowTypeInfo> rowTypes = new Dictionary<Type, RowTypeInfo>();

        internal static DataTable<TRow> Create<TRow>(string name, XmlDocument xml)
        {
            if (xml == null)
            {
                throw new ArgumentNullException("xml");
            }

            var result = new DataTable<TRow>(name);
            var nsmgr = new XmlNamespaceManager(xml.NameTable);
            nsmgr.AddNamespace("rs", DataTable.RowsetNamespace);
            nsmgr.AddNamespace("z", DataTable.RowsetSchemaNamespace);

            var rowSet = xml.SelectSingleNode("xml/rs:data", nsmgr);
            if (rowSet != null)
            {
                foreach (XmlElement row in rowSet.SelectNodes("z:row", nsmgr))
                {
                    var dataRow = DataTable.CreateRow<TRow>(row);
                    result.Add(dataRow);
                }
            }

            return result;
        }

        private static TRow CreateRow<TRow>(XmlElement rowElement)
        {
            var rowInfo = GetRowTypeInfo<TRow>();
            var row = rowInfo.CreateRow();

            foreach (XmlAttribute attribute in rowElement.Attributes)
            {
                if (rowInfo.HasColumn(attribute.Name))
                {
                    rowInfo.SetColumnValue(row, attribute.Name, attribute.Value);
                }
            }

            return (TRow)row;
        }

        private static RowTypeInfo GetRowTypeInfo<TRow>()
        {
            var rowType = typeof(TRow);
            if (!rowTypes.ContainsKey(rowType))
            {
                var rowTypeInfo = new RowTypeInfo(rowType);
                rowTypes.Add(rowType, rowTypeInfo);
            }

            return rowTypes[rowType];
        }

        private struct RowTypeInfo
        {
            private readonly Dictionary<string, ColumnInfo> columns;
            private readonly Type rowType;

            public RowTypeInfo(Type rowType)
            {
                if (rowType == null)
                {
                    throw new ArgumentNullException("rowType");
                }

                this.rowType = rowType;

                this.columns = new Dictionary<string, ColumnInfo>();
                foreach (var property in rowType.GetProperties())
                {
                    var column = new ColumnInfo(property);
                    columns.Add(column.Name, column);
                }
            }
            public bool HasColumn(string name)
            {
                return this.columns.ContainsKey(name);
            }

            public void SetColumnValue(object row, string name, string value)
            {
                this.columns[name].SetValue(row, value);
            }

            public object CreateRow()
            {
                return Activator.CreateInstance(this.rowType);
            }
        }

        private struct ColumnInfo
        {
            private readonly string columnName;
            private readonly PropertyInfo property;

            public ColumnInfo(PropertyInfo property)
            {
                if (property == null)
                {
                    throw new ArgumentNullException("property");
                }

                this.columnName = property.Name;
                this.property = property;

                var column = property.GetCustomAttribute<ColumnAttribute>();
                if (column != null && !string.IsNullOrEmpty(column.Name))
                {
                    this.columnName = column.Name;
                }
            }

            public string Name
            {
                get { return this.columnName; }
            }

            public void SetValue(object instance, string value)
            {
                if (!string.IsNullOrEmpty(value))
                {
                    var typedValue = Convert.ChangeType(value, property.PropertyType);
                    property.SetValue(instance, typedValue);
                }
            }
        }
    }
}